Imports System.Collections.Generic
Imports System.Text
Imports System.IO
Imports System.Diagnostics
Imports System.Threading

Namespace Core
	Friend MustInherit Class ClusteredFile
        Protected mRecordCount As Int32
        Friend mCurrentRecordNoPosition As Int32
        Private mRecordsByRecordNo As Dictionary(Of Int32, LinkedListNode(Of RecordHolder))
        Private mRecordsByLastUsed As LinkedList(Of RecordHolder)
        Private mConstructorThreadId As Integer
        Private mIsDisposed As Boolean = False
        ' To detect redundant calls
        Friend mRecordWidth As Integer = 0
        Friend mHeaderWidth As Integer = 0
        Private mStream As Stream
        Protected mOpenFileMode As OpenFileMode
        Protected mFileMode As FileMode
        Protected mFileAccess As FileAccess
        Protected mFileShare As FileShare
        Friend mColumns As List(Of ColumnDefinition)
        Protected Shared mOpenFiles As Dictionary(Of String, ClusteredFile)
        Private mReaderCount As Integer
        Friend mOriginalFile As String

        Const PERCENT_CACHED_RECORDS As Single = CSng(2.5 / 100)
        ' 2.5% normally
        Const MIN_CACHED_RECORDS As Integer = 100
        Const MAX_CACHED_RECORDS As Integer = 10000

        Shared Sub New()
            ' the filesystem ignore case
            mOpenFiles = New Dictionary(Of String, ClusteredFile)(StringComparer.OrdinalIgnoreCase)
        End Sub

        Protected Sub New()
            mConstructorThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId
        End Sub

        Private Sub Seek(newRecordNo As Int32)
#If DEBUG Then
            Debug.Assert(newRecordNo < Int32.MaxValue, "Invalid RecordNo")
#End If
            If newRecordNo = mCurrentRecordNoPosition Then
                Return
            End If
            mStream.Seek(newRecordNo * mRecordWidth + mHeaderWidth, SeekOrigin.Begin)
            mCurrentRecordNoPosition = newRecordNo
        End Sub

#Region "IDisposable Members"

        Protected Overridable Sub OnDisposing()
            Flush()
        End Sub

        Private Sub Dispose(disposing As Boolean)
            If mIsDisposed Then
                Return
            End If
            If disposing Then
                OnDisposing()
                mStream.Close()
                GC.SuppressFinalize(Me)
            End If
            mIsDisposed = True
            mStream = Nothing
        End Sub

        Public Overridable Sub Flush()
            If mFileAccess <> FileAccess.Read Then
                For Each r As LinkedListNode(Of RecordHolder) In mRecordsByRecordNo.Values
                    Flush(r)
                Next
                WriteHeader()
            End If
        End Sub

        Private Sub Flush(node As LinkedListNode(Of RecordHolder))
            Dim recordHolder As RecordHolder = node.Value
            If recordHolder.Record IsNot Nothing Then
                InternalFillWriteBuffer(recordHolder, recordHolder.Record)
            End If
            If recordHolder.IsModified() Then
                recordHolder.Save()
            End If
        End Sub



        Protected Overrides Sub Finalize()
            Try
                Dispose(False)
            Finally
                MyBase.Finalize()
            End Try
        End Sub

#End Region

        Protected Friend Overridable Function InternalGetRecord(recordNo As Int32, returnNullIfNotInCache As Boolean) As Record
            Debug.Assert(recordNo < Int32.MaxValue, "Invalid RecordNo")
            Dim linkListNode As LinkedListNode(Of RecordHolder) = Nothing
            Dim holder As RecordHolder = Nothing
            'Record record = null;

            If mRecordsByRecordNo.TryGetValue(recordNo, linkListNode) Then
                holder = linkListNode.Value
                'record = holder.Record;
                If holder.Record IsNot Nothing Then
                    SetLastUsed(linkListNode)
                    Return holder.Record
                Else
                    ' we're going to add a new one in one sec
                    RemoveLastUsed(linkListNode)
                End If

            ElseIf returnNullIfNotInCache Then
                Return Nothing
            End If
            ' we're here because the record was disposed or never created

            'isnew
            Dim newRecord As Record = OnCreateNewRecord(False, recordNo)


            If holder Is Nothing Then
                holder = New RecordHolder(Me, recordNo, newRecord)
                linkListNode = New LinkedListNode(Of RecordHolder)(holder)
                SetLastUsed(linkListNode)
                mRecordsByRecordNo(recordNo) = linkListNode
#If DUMP_FINALIZE Then
#End If
                System.Diagnostics.Trace.WriteLine(Me.mOriginalFile & " Save Record #" & recordNo)
            Else
                ' holder exist but object doesn't we expect that a finalized happened
                Dim ewh As EventWaitHandle = holder.RecordFinalized
                If ewh.WaitOne(10000) Then
                    ' we (re)connect the recordHolder to the new Record
                    ewh.Reset()
                    holder.Record = newRecord
                Else
                    Throw New Exception("Something very wrong happened.")
                End If
            End If
            newRecord.RecordHolder = holder

            'readIfNeeded
            Dim sourceBuffer As [Byte]() = holder.GetCurrentBuffer(True)
            OnReadRecordBuffer(sourceBuffer, newRecord)

            Return newRecord
        End Function

		Private Sub RemoveLastUsed(linkListNode As LinkedListNode(Of RecordHolder))
			If linkListNode.List Is mRecordsByLastUsed Then
				mRecordsByLastUsed.Remove(linkListNode)
			End If
		End Sub

		Private Sub SetLastUsed(linkListNode As LinkedListNode(Of RecordHolder))
			If linkListNode.List Is mRecordsByLastUsed Then
				If linkListNode IsNot mRecordsByLastUsed.Last Then
					mRecordsByLastUsed.Remove(linkListNode)
					mRecordsByLastUsed.AddLast(linkListNode)
				End If
			Else
				mRecordsByLastUsed.AddLast(linkListNode)
			End If

			' for every table we use PERCENT_CACHED_RECORDS
			Dim limit As Integer = CInt(Math.Truncate(PERCENT_CACHED_RECORDS * mRecordCount))
			If limit < MIN_CACHED_RECORDS Then
				limit = MIN_CACHED_RECORDS
			Else
				If limit > MAX_CACHED_RECORDS Then
					limit = MAX_CACHED_RECORDS
				End If
			End If
            Dim node As LinkedListNode(Of RecordHolder) = mRecordsByLastUsed.First
			While node IsNot Nothing AndAlso mRecordsByLastUsed.Count > limit
				Dim recordHolder As RecordHolder = node.Value
                Dim nextNode As LinkedListNode(Of RecordHolder) = node.[Next]

				node.Value.Hold = False
				mRecordsByLastUsed.Remove(node)
				#If DUMP_FINALIZE Then
				System.Diagnostics.Trace.WriteLine(Me.mOriginalFile & " Remove last used Record #" & Convert.ToString(recordHolder.mRecordNo))
				#End If
				node = nextNode
			End While
		End Sub



		''' <summary>
		''' Note that this method is called from flush and from the record destructor in the garbage collector thread.
		''' </summary>
		''' <param name="recordHolder"></param>
		Friend Sub InternalFillWriteBuffer(recordHolder As RecordHolder, record As Record)
			Debug.Assert(record IsNot Nothing, "InternalFillWriteBuffer : record must be not null.")
			Dim newBuffer As Byte() = New Byte(mRecordWidth - 1) {}

			If OnFillWriteBuffer(record, newBuffer) Then
				recordHolder.SetNewBuffer(newBuffer)
			End If
		End Sub

        Protected MustOverride Function OnCreateNewRecord(isNew As Boolean, recordNo As Int32) As Record
		Protected MustOverride Sub OnReadRecordBuffer(buffer As Byte(), record As Record)
		Friend MustOverride Function OnFillWriteBuffer(record As Record, buffer As [Byte]()) As Boolean
		Protected MustOverride Sub OnInitialize(mStream As Stream)
		Protected MustOverride Sub OnWriteHeader(stream As Stream)

        Friend Overridable Sub OnWriteBufferModified(recordNo As Int32, oldBuffer As Byte(), newBuffer As Byte())
        End Sub

		Public Overridable Function NewRecord() As Object
            Dim recordNo As Integer = mRecordCount
            mRecordCount += 1
			'isnew
            Dim result As Record = OnCreateNewRecord(True, recordNo)

            Dim holder As RecordHolder = New RecordHolder(Me, recordNo, result)
            Dim linkedListNode As LinkedListNode(Of RecordHolder) = New LinkedListNode(Of RecordHolder)(holder)

            result.RecordHolder = holder

			mRecordsByRecordNo(recordNo) = linkedListNode
			SetLastUsed(linkedListNode)
			#If DUMP_FINALIZE Then
			System.Diagnostics.Trace.WriteLine(Me.mOriginalFile & " Add Record #" & recordNo)
			#End If


			Return result
		End Function

		Friend Function GetColumnByName(columnName As String) As ColumnDefinition
			For i As Integer = 0 To mColumns.Count - 1
                If mColumns(i).ColumnName = columnName Then
                    Return mColumns(i)
                End If
			Next
			' not found try again with case off
			For i As Integer = 0 To mColumns.Count - 1
                If String.Compare(mColumns(i).ColumnName, columnName, True) = 0 Then
                    Return mColumns(i)
                End If
			Next
			Return Nothing
		End Function

		Public Shared Function [Get](Of T As {ClusteredFile, New})(filepath As String, mode As OpenFileMode, bufferSize As Integer) As T
            Dim fi As FileInfo = New System.IO.FileInfo(filepath)
			' in case the file is readonly we assume users expect us to force the OpenReadOnly mode.
			If fi.Exists AndAlso (fi.Attributes And FileAttributes.[ReadOnly]) <> 0 Then
				mode = OpenFileMode.OpenReadOnly
			End If
			Dim clusteredFile As ClusteredFile = Nothing
			If Not mOpenFiles.TryGetValue(fi.FullName, clusteredFile) Then
				clusteredFile = New T()
				mOpenFiles.Add(fi.FullName, clusteredFile)
			End If
			Dim fm As FileMode
			Dim fa As FileAccess
			Dim fs As FileShare
			Select Case mode
				Case OpenFileMode.OpenOrCreate
					fm = FileMode.OpenOrCreate
					fa = FileAccess.ReadWrite
					fs = FileShare.Read
					Exit Select
				Case OpenFileMode.OpenReadOnly
					fm = FileMode.Open
					fa = FileAccess.Read
					fs = FileShare.ReadWrite
					Exit Select
				Case Else
					' OpenFileMode.OpenReadWrite:
					fm = FileMode.Open
					fa = FileAccess.ReadWrite
					fs = FileShare.Read
					Exit Select
			End Select
			clusteredFile.mOpenFileMode = mode
			clusteredFile.InternalAddReader(fi, fm, fa, fs, bufferSize)
			Return DirectCast(clusteredFile, T)
		End Function

		Private Sub InternalAddReader(fi As FileInfo, fm As FileMode, fa As FileAccess, fs As FileShare, bufferSize As Integer)
			If mStream Is Nothing Then
				mStream = New FileStream(fi.FullName, fm, fa, fs, bufferSize)
				mFileMode = fm
				mFileAccess = fa
				mFileShare = fs

				'if (encoding == null) encoding = System.Text.Encoding.ASCII;
				'mEncoding = encoding;
				mOriginalFile = fi.FullName
                mCurrentRecordNoPosition = Int32.MaxValue
                mRecordsByRecordNo = New Dictionary(Of Int32, LinkedListNode(Of RecordHolder))()
				#If DUMP_FINALIZE Then
				System.Diagnostics.Trace.WriteLine(Me.mOriginalFile & "Clear Records")
				#End If
				mRecordsByLastUsed = New LinkedList(Of RecordHolder)()
					' todo check that the access make sense.
			Else
			End If
			mReaderCount += 1
		End Sub

		Public Sub StopReading()
			mReaderCount -= 1
			If mReaderCount = 0 Then
				Dispose(True)
			End If
		End Sub

		Public ReadOnly Property ReaderCount() As Integer
			Get
				Return mReaderCount
			End Get
		End Property

		Protected Sub WriteHeader()
			mStream.Position = 0
			OnWriteHeader(mStream)
            mCurrentRecordNoPosition = Int32.MaxValue
		End Sub

		Protected Friend Sub Initialize()
			OnInitialize(mStream)
			System.Diagnostics.Debug.Assert(mRecordWidth > 0, "RecordWidth should be initialized by now.")
		End Sub

		Friend Sub EmptyTable()
            Dim node As LinkedListNode(Of RecordHolder) = mRecordsByLastUsed.First

			While node IsNot Nothing AndAlso mRecordsByLastUsed.Count > 0
				Dim recordHolder As RecordHolder = node.Value
                Dim nextNode As LinkedListNode(Of RecordHolder) = node.[Next]
				recordHolder.Save()
				node = nextNode
			End While
			#If DUMP_FINALIZE Then
			System.Diagnostics.Trace.WriteLine(Me.mOriginalFile & " Clear Records")
			#End If

			mRecordsByRecordNo.Clear()
			mRecordsByLastUsed.Clear()
			mRecordCount = 0
			mStream.SetLength(mHeaderWidth)
		End Sub
        Friend Sub InternalReadRecordFromDisk(recordNo As Int32, buffer As [Byte]())
#If DEBUG Then
            System.Diagnostics.Debug.Assert(recordNo < Int32.MaxValue, "Invalid Record Number.")
#End If
            Seek(recordNo)
            mStream.Read(buffer, 0, mRecordWidth)
            mCurrentRecordNoPosition = recordNo + 1
#If DUMP_DISK_ACTIVITY Then
			System.Diagnostics.Debug.WriteLine(mOriginalFile & " ReadFromDisk Record #" & recordNo)
			System.Diagnostics.Debug.WriteLine(Utils.HexDump(buffer))
#End If
        End Sub
        Friend Sub InternalSaveRecordToDisk(recordNo As Int32, buffer As [Byte]())
            Seek(recordNo)
            mStream.Write(buffer, 0, mRecordWidth)
            mCurrentRecordNoPosition = recordNo + 1
#If DUMP_DISK_ACTIVITY Then
			System.Diagnostics.Debug.WriteLine(mOriginalFile & " SaveToDisk Record #" & recordNo)
			System.Diagnostics.Debug.WriteLine(Utils.HexDump(buffer))
#End If
        End Sub
        Protected Function ReadNextPage(buffer As Byte()) As Boolean
            Dim read As Integer = mStream.Read(buffer, 0, mRecordWidth)
            If buffer.Length = mRecordWidth AndAlso mCurrentRecordNoPosition <> Int32.MaxValue Then
                mCurrentRecordNoPosition += 1
            Else
                mCurrentRecordNoPosition = Int32.MaxValue
            End If
            Return read > 0
        End Function
	End Class
End Namespace

